﻿using System;
using System.Xml;
using System.IO;
using System.Xml.XPath;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.ComponentModel;
using System.Linq;

namespace Relay.Core.Configuration {
    /// <summary>
    /// A service class which is used to read, manipulate, and store configuration information
    /// about the application.
    /// </summary>
    public class ConfigurationService {
        private const string RootElement = "relay-configuration";
        private const string ListValueElement = "list-value";

        #region Fields
        private XmlDocument _settings;
        private XmlNode _root;
        #endregion

        #region Constructor
        private ConfigurationService() { }
        #endregion

        #region Private Methods
        /// <summary>
        /// Initializes a new settings file.
        /// </summary>
        private void NewSettingsFile() {
            XmlDocument doc = new XmlDocument();
            doc.AppendChild(doc.CreateXmlDeclaration("1.0", null, null));
            _root = doc.AppendChild(doc.CreateElement(RootElement));

            _settings = doc;
        }

        /// <summary>
        /// Converts a list to a XML.
        /// </summary>
        /// <typeparam name="T">The type of list to convert.</typeparam>
        /// <param name="list">The list.</param>
        /// <param name="key">The desired name of the list element.</param>
        /// <returns></returns>
        private string ListToXml<T>(List<T> list) {
            XmlNode node = _settings.CreateElement("dummy");
            foreach (var o in list) {
                XmlNode child = _settings.CreateElement(ListValueElement);
                child.InnerText = o.ToString();
                node.AppendChild(child);
            }

            return node.InnerXml;
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Gets the specified setting from the configuration file.
        /// </summary>
        /// <typeparam name="T">The type of setting to get.</typeparam>
        /// <param name="category">The category.</param>
        /// <param name="key">The key.</param>
        /// <param name="defValue">The default value.</param>
        /// <returns>The value of the specified setting.</returns>
        public T Get<T>(string category, string key, T defValue) {
            XmlNode node = _settings.SelectSingleNode(String.Format("/{0}/{1}/{2}", RootElement,
                                                                    category, key));
            if (node == null) return defValue;

            TypeConverter conv = TypeDescriptor.GetConverter(typeof(T));

            if (!conv.CanConvertFrom(typeof(string)))
                throw new InvalidOperationException("No valid conversion to the specified type.");

            return (T)conv.ConvertFrom(node.InnerText);
        }

        /// <summary>
        /// Gets the specified setting from the configuration file.
        /// </summary>
        /// <typeparam name="T">The type of setting to get.</typeparam>
        /// <param name="category">The category.</param>
        /// <param name="key">The key.</param>
        /// <returns>The value of the specified setting.</returns>
        public T Get<T>(string category, string key) {
            return Get(category, key, default(T));
        }

        /// <summary>
        /// Sets the specified setting in the configuration file.
        /// </summary>
        /// <typeparam name="T">The type of setting to set.</typeparam>
        /// <param name="category">The category.</param>
        /// <param name="key">The key.</param>
        /// <param name="value">The value of the specified setting.</param>
        public void Set<T>(string category, string key, T value) {
            Set(category, key, value, false);
        }

        /// <summary>
        /// Sets the specified setting in the configuration file.
        /// </summary>
        /// <typeparam name="T">The type of setting to set.</typeparam>
        /// <param name="category">The category.</param>
        /// <param name="key">The key.</param>
        /// <param name="value">The value of the specified setting.</param>
        /// <param name="rawXml">if set to <c>true</c> this setting is raw XML, otherwise text.</param>
        protected void Set<T>(string category, string key, T value, bool rawXml) {
            XmlNode categoryNode = _settings.SelectSingleNode(String.Format("/{0}/{1}", RootElement,
                                                                    category, key));
            if (categoryNode == null) {
                categoryNode = _settings.CreateElement(category);
                _root.AppendChild(categoryNode);
            }

            XmlNode settingNode = categoryNode.SelectSingleNode(key);
            if (settingNode == null) {
                settingNode = _settings.CreateElement(key);
                categoryNode.AppendChild(settingNode);
            }

            TypeConverter conv = TypeDescriptor.GetConverter(typeof(T));

            if (rawXml)
                settingNode.InnerXml = value.ToString();
            else
                settingNode.InnerText = conv.ConvertToString(value);
        }

        /// <summary>
        /// Gets the specified setting list from the configuration file.
        /// </summary>
        /// <typeparam name="T">The type of list to get.</typeparam>
        /// <param name="category">The category.</param>
        /// <param name="key">The key.</param>
        /// <returns></returns>
        public List<T> GetList<T>(string category, string key) {
            XmlNode list = _settings.SelectSingleNode(String.Format("/{0}/{1}/{2}", RootElement, category, key));
            List<T> obj;

            if (list == null) return null;

            try {
                obj = (from XmlNode node in list.ChildNodes
                               where node.Name == ListValueElement
                               select (T)Convert.ChangeType(node.InnerText, typeof(T))).ToList();
            } catch (InvalidCastException e) {
                throw new InvalidOperationException("No valid conversion to the specified type.", e);
            }

            return obj;
        }

        public void SetList<T>(string category, string key, List<T> value) {
            string xml = ListToXml(value);
            Set(category, key, xml, true);
        }

        /// <summary>
        /// Loads the specified configuration file.
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <returns></returns>
        public static ConfigurationService Load(string filename) {
            ConfigurationService obj = new ConfigurationService();
            XmlNode root;

            try {
                obj._settings = new XmlDocument();
                obj._settings.Load(filename);

                root = obj._settings.SelectSingleNode(String.Format("/{0}", RootElement));
                if (root == null)
                    throw new XmlException("Loaded XML file is not a valid configuration file.");

                obj._root = root;
            } catch (XmlException) {
                File.Copy(filename, String.Format("{0}_malformed", filename), true);
                obj.NewSettingsFile();
            } catch (IOException) {
                obj.NewSettingsFile();
            }

            return obj;
        }

        /// <summary>
        /// Saves the configuration to the specified file.
        /// </summary>
        /// <param name="filename">The filename.</param>
        public void Save(string filename) {
            _settings.Save(filename);
        }
        #endregion
    }
}
